Фильтрация и итерация в read view с помощью CRUD¶
В этом разделе приведены подробные примеры использования операций select
и pairs
для read view с помощью
модуля CRUD.
Содержание:
Пререквизиты¶
Для выполнения примера требуются:
установленный Docker-образ Tarantool DB;
приложение Docker Compose;
утилита tt CLI;
исходные файлы примера
read_view
.Примечание
Есть два способа получить исходные файлы примера:
Архив с полной документацией Tarantool DB, полученный по почте или скачанный в личном кабинете tarantool.io. Пример архива:
tarantooldb-documentation-2.0.0.tar.gz
. Примерread_view
расположен в таком архиве в директории./doc/examples/read_view/
.Отдельный архив read_view.tar.gz, скачанный c сайта Tarantool.
Используемые файлы¶
В руководстве используются следующие файлы примера read_view
:
cluster/
– директория c файлами для запуска кластера Tarantool DB:config.yml
– конфигурация и топология кластера;docker-compose.yml
– описание узлов кластера Tarantool DB;migrations/scenario
– директория, содержащая файлы с описанием миграций;
tools/
– директория с файлами для запуска кластера etcd и TCM:docker-compose.yml
– описание узлов кластера etcd;tcm.yml
– конфигурация для запуска Tarantool Cluster Manager.
Запуск стенда¶
Для успешного запуска должны быть свободны следующие порты:
3301–3306
8081
2379
Перейдите в директорию примера read_view
:
cd ./doc/examples/read_view/
Запустите стенд через Docker Compose:
make start
Команда развернет стенд, состоящий из:
кластера Tarantool DB:
1 роутер;
2 набора реплик по 2 хранилища;
кластера etcd из 3 узлов.
После запуска должны работать все контейнеры, кроме init_host.
Также после запуска кластера становится доступен веб-интерфейс TCM. Для входа в TCM откройте в браузере адрес http://localhost:8081. Логин и пароль для входа:
Username:
admin
Password:
secret
В TCM откройте вкладку Stateboard.
Выберите в наборе реплик router-1
узел router-1
и в открывшемся окне перейдите на вкладку Terminal.
Во вкладке Terminal проверьте наличие спейса customers
:
box.space
Спейс customers
должен присутствовать в выводе, он создается при запуске кластера.
Перейдите на вкладку Tuples
.
Проверьте, что во вкладке отображается спейс customers
, и в этот спейс загружены данные.
Создание спейса и подключение к узлу¶
На завершающем этапе поднятия кластера выполняется публикация YAML-конфигурации кластера в централизованное хранилище
и применяются миграции.
Миграции создают спейс customers
(файл ./cluster/migrations/scenario/001_create_space.lua
) и
загружают в него данные (файл ./cluster/migrations/scenario/002_data.lua
).
Спейс имеет следующий формат:
box.schema.space.create('customers', {if_not_exists = true})
box.space.customers:format({
{ name = 'id', type = 'integer' },
{ name = 'bucket_id', type = 'unsigned' },
{ name = 'name', type = 'string' },
{ name = 'surname', type = 'string' },
{ name = 'age', type = 'number' },
})
box.space.customers:create_index('pk', { parts = {'id'}, if_not_exists = true})
box.space.customers:create_index('bucket_id', { parts = {'bucket_id'}, unique = false, if_not_exists = true})
box.space.customers:create_index('age_index', { parts = {'age'}, unique = false, if_not_exists = true})
box.space.customers:create_index('full_name', { parts = {'name', 'surname'}, unique = false, if_not_exists = true})
Чтобы начать работу с базой данных через интерактивную консоль Tarantool, нужно подключиться к узлу кластера. Сделать это можно двумя способами:
в веб-интерфейсе TCM;
в терминале с помощью утилиты tt CLI:
tt connect admin:secret-cluster-cookie@localhost:3301
Подключитесь к роутеру router-1
, используя первый способ – через TCM. Для этого:
Перейдите на вкладку Stateboard.
Нажмите на набор реплик
router-1
.Выберите роутер
router-1
и в открывшемся окне перейдите на вкладку Terminal.
Создание представления для чтения¶
Чтобы создать read view, во вкладке Terminal вызовите функцию crud.readview()
:
rv = crud.readview()
Фильтрация кортежей с помощью select¶
Метод read_view_object:select()
позволяет фильтровать кортежи по условиям.
Каждое условие должно использовать имя поля или имя индекса.
Первое условие с именем индекса используется для итерации по спейсам.
Если условия с именами индексов отсутствуют, выполняется полное сканирование (Map-Reduce).
Остальные условия используются в качестве дополнительных фильтров.
Условие поиска для индексируемого поля должно быть размещено первым, чтобы избежать
полного сканирования.
Чтобы избежать длинных выборок, можно ограничить количество результатов с помощью параметра first
.
Примечание
Если вы укажете ключ шардирования (bucket_id
), операция select
будет выполнена на одном узле.
В противном случае произойдет Map-Reduce по всем узлам.
В примере ниже получены первые 6 кортежей из спейса customers
:
rv:select('customers', nil, { first = 6 })
Запрос возвращает метаданные, массив кортежей и ошибку:
---
- metadata: [ { 'name': 'id', 'type': 'integer' }, { 'name': 'bucket_id', 'type': 'unsigned' },
{ 'name': 'name', 'type': 'string' }, { 'name': 'surname', 'type': 'string' }, { 'name': 'age',
'type': 'number' } ]
rows:
- [ 1, 12477, 'Elizabeth', 'Bagnall', 12 ]
- [ 2, 21401, 'Mary', 'Bowman', 46 ]
- [ 3, 11804, 'David', 'Bradley', 33 ]
- [ 4, 28161, 'William', 'Bridgens', 81 ]
- [ 5, 1172, 'Jack', 'Brown', 35 ]
- [ 6, 13064, 'William', 'Long', 25 ]
- null
...
Запрос по простому индексу¶
В примере по индексу age_index
выбраны первые 10 клиентов, возраст которых больше или равен 20.
Так как первое условие – индекс age_index
, результат отсортирован по возрасту.
rv:select('customers', { { '>=', 'age_index', 20 } }, { first = 10 })
---
- metadata: [ { 'name': 'id', 'type': 'integer' }, { 'name': 'bucket_id', 'type': 'unsigned' },
{ 'name': 'name', 'type': 'string' }, { 'name': 'surname', 'type': 'string' }, { 'name': 'age',
'type': 'number' } ]
rows:
- [ 40, 6292, 'Evelyn', 'Mishra', 20 ]
- [ 12, 16624, 'Olivia', 'Kinsella', 22 ]
- [ 25, 158, 'Ava', 'Fisher', 23 ]
- [ 34, 9834, 'Amelia', 'Ahmed', 24 ]
- [ 6, 13064, 'William', 'Long', 25 ]
- [ 23, 28454, 'Mia', 'Morrison', 25 ]
- [ 20, 3826, 'Abigail', 'Gauld', 26 ]
- [ 29, 17582, 'Chloe', 'Walters', 27 ]
- [ 14, 24056, 'Emily', 'Grimshaw', 28 ]
- [ 38, 26474, 'Avery', 'Murray', 28 ]
- null
...
Здесь запросы по индексу age_index
и полю age
эквивалентны, в обоих случаях поиск будет осуществляться по индексу без полного сканирования:
rv:select('customers', { { '>=', 'age_index', 20 } }, { first = 10 })
rv:select('customers', { { '>=', 'age', 20 } }, { first = 10 })
Если имена индекса и поля совпадают, поиск также идет по индексу.
Запрос по составному индексу¶
В примере используется составной индекс full_name
, состоящий из полей name
и surname
.
Запрос возвращает 10 первых клиентов, имя и фамилия которых совпадают с условием:
rv:select('customers', { { '==', 'full_name', { 'Thomas', 'Griffin' } } }, { first = 10 })
---
- metadata: [ { 'name': 'id', 'type': 'integer' }, { 'name': 'bucket_id', 'type': 'unsigned' },
{ 'name': 'name', 'type': 'string' }, { 'name': 'surname', 'type': 'string' }, { 'name': 'age',
'type': 'number' } ]
rows:
- [ 13, 14925, 'Thomas', 'Griffin', 64 ]
- null
...
В запросе также можно использовать часть составного ключа:
rv:select('customers', { { '==', 'full_name', 'William' } }, { first = 10 })
---
- metadata: [ { 'name': 'id', 'type': 'integer' }, { 'name': 'bucket_id', 'type': 'unsigned' },
{ 'name': 'name', 'type': 'string' }, { 'name': 'surname', 'type': 'string' }, { 'name': 'age',
'type': 'number' } ]
rows:
- [ 4, 28161, 'William', 'Bridgens', 81 ]
- [ 6, 13064, 'William', 'Long', 25 ]
- [ 22, 21655, 'William', 'Norman', 42 ]
- null
...
Примечание
Если указать частичный ключ не для первого параметра, например { '==', 'full_name', {nil, 'Griffin'}
, то будет
выполнено полное сканирование (Map-Reduce).
Запрос по полю без индекса¶
Если указать в запросе не индексируемое поле, будет выполнено полное сканирование (Map-Reduce).
В примере выбраны первые 10 покупателей с заданной фамилией:
rv:select('customers', { { '==', 'surname', 'Wilcox' } }, { first = 10 })
---
- metadata: [ { 'name': 'id', 'type': 'integer' }, { 'name': 'bucket_id', 'type': 'unsigned' },
{ 'name': 'name', 'type': 'string' }, { 'name': 'surname', 'type': 'string' }, { 'name': 'age',
'type': 'number' } ]
rows:
- [ 30, 29239, 'Jacob', 'Wilcox', 49 ]
- null
...
Итерация с помощью pairs¶
Метод read_view_object:pairs()
позволяет итерироваться по распределенному спейсу.
В примере записаны в таблицу в виде кортежа первые 4 записи:
tuples = {}
for _, tuple in rv:pairs('customers', nil, { first = 4 }) do
table.insert(tuples, tuple)
end
tuples
Вывод выглядит так:
---
- - [ 1, 12477, 'Elizabeth', 'Bagnall', 12 ]
- [ 2, 21401, 'Mary', 'Bowman', 46 ]
- [ 3, 11804, 'David', 'Bradley', 33 ]
- [ 4, 28161, 'William', 'Bridgens', 81 ]
...
Параметр use_tomap¶
Чтобы итерироваться по объектам или плоским кортежам, используйте параметр use_tomap
со значением true
.
В примере в таблицу записаны в виде объектов первые 4 покупателя:
objects = {}
for _, obj in rv:pairs('customers', nil, { use_tomap = true, first = 4 }) do
table.insert(objects, obj)
end
objects
Вывод выглядит так:
---
- - bucket_id: 12477
id: 1
surname: Bagnall
age: 12
name: Elizabeth
- bucket_id: 21401
id: 2
surname: Bowman
age: 46
name: Mary
- bucket_id: 11804
id: 3
surname: Bradley
age: 33
name: David
- bucket_id: 28161
id: 4
surname: Bridgens
age: 81
name: William
...
Lua Fun¶
Операция pairs
совместима с библиотекой Lua Fun.
Примеры работы с основными функциями из этой библиотеки приведены ниже.
Filter¶
Функция filter() возвращает новый итератор, элементы которого удовлетворяют предикату. В примере функция-предикат возвращает элементы, у которых значение поля возраста делится на 2 без остатка.
objects = {}
for _, obj in rv:pairs('customers', nil, { first = 4, use_tomap = true }):filter(function(x)
return x.age % 2 == 0
end) do
table.insert(objects, obj)
end
objects
Вывод выглядит так:
---
- - bucket_id: 12477
id: 1
surname: Bagnall
age: 12
name: Elizabeth
- bucket_id: 21401
id: 2
surname: Bowman
age: 46
name: Mary
...
Reduce (foldl)¶
Функция reduce (foldl) уменьшает итератор слева направо:
age_sum = crud.pairs('customers', nil, { use_tomap = true }):reduce(function(acc, x)
return acc + x.age
end, 0)
age_sum
Вывод выглядит так:
---
- 1441
...
Map¶
Функция map() возвращает новый итератор, к каждому элементу которого была применена функция.
objects = {}
for _, obj in rv:pairs('customers', nil, { first = 4, use_tomap = true }):map(function(x)
return { id = x.id, name = x.name, age = x.age * 2 }
end) do
table.insert(objects, obj)
end
objects
Вывод выглядит так:
---
- - age: 24
name: Elizabeth
id: 1
- age: 92
name: Mary
id: 2
- age: 66
name: David
id: 3
- age: 162
name: William
id: 4
...
Take¶
Функция take() возвращает итератор с заданным количеством последовательностей:
tuples = {}
for _, tuple in rv:pairs('customers', { { '>=', 'age', 25 } }):take(2) do
table.insert(tuples, tuple)
end
tuples
Вывод выглядит так:
---
- - [ 6, 13064, 'William', 'Long', 25 ]
- [ 23, 28454, 'Mia', 'Morrison', 25 ]
...
Остановка стенда¶
Чтобы остановить стенд, выполните в локальном терминале следующую команду:
make stop